﻿/*
 * Name:        Worldspawn
 * Description: A set of classes that define the world and gamerules
 * 
 * Containts:
 *      Game        - Everyrything is managed by this class, only one may exist at the time
 *      GameRules   - Set of rules by which the world sends feedback
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Xml;
using System.Xml.Linq;
using System.Media;

using Gyvate;
using Gyvate.Field;
using Gyvate.Util;
using Gyvate.Entities;
using Gyvate.InputNS;
using Gyvate.GFX;
using Gyvate.Collision;

namespace Gyvate.Worldspawn
{
    internal class GameRules
    {
        private bool m_bGameOver;
        private int m_iGameSpeed;

        private int m_iSpotlightSize;
        private bool m_bSpotlightEnabled;
        private float m_fSpotlightFactor;
        private Difficulty m_iDifficulty;
        private int m_iScoreDivider;
        private int m_iSpeedUp;

        private int m_iEaten;
        private int m_iScore;

        private Game m_game;

        public bool IsWallSolid() { return false; }                     /* DEPRECATED!
                                                                         * No longer needed, but there are instatances
                                                                         * in code where it is used and actually
                                                                         * changes gameplay. Maybe I will clean up the code
                                                                         * someday */

        public bool IsGameOver() { return m_bGameOver; }                //Returns true if game was lost
        public void IncrementEaten() { m_iEaten++; }                    //Increments eaten by one

        public int GetEaten() { return m_iEaten; }                      //Get food eaten
        public int GetSpeed() { return m_game.GetTimer().Interval; }    //Get current speed
        public int GetScore() { return m_iScore; }                      //Returns current score
        public int GetSpotLightSize() { return m_iSpotlightSize; }      //Returns spotlight sizeMainPanel
        public bool IsSpotlightEnabled() { return m_bSpotlightEnabled; }
        public int GetScoreDiv() { return m_iScoreDivider; }
        public int GetSpeedUp() { return m_iSpeedUp; }

        /*
         * Constructor
         */
        public GameRules(Game game, Difficulty dif, bool spotlight)
        {
            m_game = game;

            m_iDifficulty = dif;
            m_bSpotlightEnabled = spotlight;

            if (m_game.IsDebug())
                m_bSpotlightEnabled = false;

            m_bGameOver = false;
            m_iEaten = 0;
            m_iScore = 0;

            ApplyDifficultySettings();

            SetNewSpeed(m_iGameSpeed);
        }

        private void ApplyDifficultySettings()
        {
            switch (m_iDifficulty)
            {
                case Difficulty.DIFFICULTY_EASY:
                    m_iGameSpeed = Constants.EASY_SPEED;
                    m_iSpotlightSize = Constants.EASY_SPOTLIGHT;
                    m_fSpotlightFactor = Constants.EASY_SPOTLIGHT_SHRINK_FACTOR;
                    m_iScoreDivider = Constants.EASY_SCORE_DIVIDER;
                    m_iSpeedUp = Constants.EASY_SPEED_UP;
                    break;

                case Difficulty.DIFFICULTY_NORMAL:
                default:
                    m_iGameSpeed = Constants.NORMAL_SPEED;
                    m_iSpotlightSize = Constants.NORMAL_SPOTLIGHT;
                    m_fSpotlightFactor = Constants.NORMAL_SPOTLIGHT_SHRINK_FACTOR;
                    m_iScoreDivider = Constants.NORMAL_SCORE_DIVIDER;
                    m_iSpeedUp = Constants.NORMAL_SPEED_UP;
                    break;

                case Difficulty.DIFFICULTY_HARD:
                    m_iGameSpeed = Constants.HARD_SPEED;
                    m_iSpotlightSize = Constants.HARD_SPOTLIGHT;
                    m_fSpotlightFactor = Constants.HARD_SPOTLIGHT_SHRINK_FACTOR;
                    m_iScoreDivider = Constants.HARD_SCORE_DIVIDER;
                    m_iSpeedUp = Constants.HARD_SPEED_UP;
                    break;
            }
        }

        /*
         * Sets new speed for the game to tick at
         */
        public void SetNewSpeed(int newSpeed)
        {
            m_game.GetTimer().Enabled = true;

            if (newSpeed == 0)
                m_game.GetTimer().Enabled = false;
            else
                m_game.GetTimer().Interval = newSpeed;

            m_iGameSpeed = newSpeed;
        }

        /*
         * Sets pause state
         */
        public void SetPauseState(bool state)
        {
            if (!m_bGameOver)
                m_game.GetTimer().Enabled = !state;
        }

        /*
         * Sets game over state
         */
        public void SetGameOver()
        {
            m_bGameOver = true;
            SetNewSpeed(0);
        }

        /*
         * Adds score
         */
        public void AddPoints(int pts = 1)
        {
            m_iScore += pts;

            if ((m_iGameSpeed - GetSpeedUp()) > Constants.SPEED_MIN)
                SetNewSpeed(m_iGameSpeed - GetSpeedUp());
            else
                SetNewSpeed(Constants.SPEED_MIN);

            int newSpotlightSize = (int)Math.Floor(m_iSpotlightSize * m_fSpotlightFactor);

            if (newSpotlightSize >= Constants.SPOTLIGHT_MIN_SIZE)
                m_iSpotlightSize = newSpotlightSize;
        }
    }

    internal class Game
    {
        private PlayField m_playField;
        private Snake m_snake;
        private Input m_input;
        private GameRules m_gameRules;
        private bool m_bDebug;
        private bool m_bGamePaused;
        private bool m_bMenuVisible;
        private bool m_bEscapeReleased = true;
        private bool m_bNeedMouseReset = true;

        private GFXBuffer m_fx;
        private GFXBuffer m_spotlight;
        private GFXBuffer m_gui;
        private System.Windows.Forms.Timer m_timer;
        private GameForm m_parent;
        private Random m_rng;
        private UserInterface m_ui;

        private BaseEntity m_background;
        private TileList m_iCurrentBuild = TileList.TILE_NONE;

        private Dictionary<string, PrecachedSprite> m_precahcedSprites;
        private CollisionManager m_collisionManager;

        public System.Windows.Forms.Timer GetTimer() { return m_timer; }                //Gets timer
        public GameRules GetGameRules() { return m_gameRules; }                         //Gets game rules
        public Input GetInput() { return m_input; }                                     //Gets input
        public int GetRandomNumber(int min, int max) { return m_rng.Next(min, max); }   //Gets random integer in bounds
        public CollisionManager GetCollisionManager() { return m_collisionManager; }    //Gets collision manager
        public Snake GetSnake() { return m_snake; }                                     //Gets snake
        public PlayField GetPlayField() { return m_playField; }                         //Gets playing field
        public Graphics GetGraphics() { return m_fx.GetFX(); }                          //Gets graphics
        public Graphics GetUIParent() { return m_gui.GetFX(); }                         //Gets graphics for UI
        public UserInterface GetUI() { return m_ui; }                                   //Gets UI

        public bool IsDebug() { return m_bDebug; }                                      //Gets debug state
        public bool IsPaused() { return m_bGamePaused; }                                //Gets pause state

        public void SetCurrentBuild(TileList tile) { m_iCurrentBuild = tile; }          //Set tile that is being built

        public bool IsMouseReset() { return !m_bNeedMouseReset; }                        //Check if mouse was reset
        
        /*
         * Constructor
         */
        public Game(GameForm parent, bool debug = false)
        {
            m_parent = parent;
            m_bDebug = debug;

            m_precahcedSprites = new Dictionary<string, PrecachedSprite>();
            m_ui = new UserInterface(this);

            Vector2d size = new Vector2d();

            size.x = (Constants.UI_WIDTH) * Constants.SPRITE_SIZE;
            size.y = (Constants.UI_HEIGHT) * Constants.SPRITE_SIZE;

            m_gui = new GFXBuffer(size);

            m_timer = new System.Windows.Forms.Timer();
            m_timer.Tick += new System.EventHandler(RunGame);
            m_rng = new Random();

            m_playField = new PlayField(this, new Vector2d(Constants.FIELD_WIDTH, Constants.FIELD_HEIGHT));

            LoadMap();
            m_playField.RebuildWorld();

            Reset();
        }

        /*
         * Resets the game
         */
        private void Reset()
        {
            m_bGamePaused = false;
            
            m_fx = new GFXBuffer();
            m_spotlight = new GFXBuffer();

            m_gameRules = new GameRules(this, (Difficulty)m_parent.GetDifficulty(), m_parent.IsSpotLightEnabled());

            m_input = new Input();

            Vector2d spawnPos = new Vector2d(Constants.FIELD_WIDTH/2, Constants.FIELD_HEIGHT/2);
            int rotation = 0;

            if (m_playField.GetSpawnPoint() != null)
            {
                spawnPos = m_playField.GetSpawnPoint().GetPos();
                rotation = m_playField.GetSpawnPoint().GetRotation();
            }

            m_snake = new Snake(this, new Vector2d(spawnPos), rotation, 3);
            
            m_collisionManager = new CollisionManager(this);

            PrecacheSprite(Constants.SPRITE_BACKGROUND);
            m_background = new BaseEntity(new Vector2d(m_playField.GetPos().x, m_playField.GetPos().y), this);
            m_background.SetSprite(Constants.SPRITE_BACKGROUND);

            if (m_playField != null)
                m_playField.Reset();

            if( !IsDebug() )
                PlaySound(Constants.SOUND_RESTART);
        }

        /*
         * Runs the game
         */
        public void RunGame(object sender, EventArgs e)
        {
            if (!m_bGamePaused)
            {
                PreThink();
                Think();
                PostThink();
            }
        }

        /*
         * Handles received events
         * TODO: Change the way events work, to allow parameters, for when we have more pickup entities
         */
        public void SendEvent(EventList ev)
        {
            switch (ev)
            {
                case EventList.EVENT_UNKNOWN:
                default:
                    break;

                case EventList.EVENT_NEWGAME:
                    break;

                case EventList.EVENT_GAMEOVER:
                    Console.WriteLine("Game over! Points collected: " + m_gameRules.GetScore());
                    GetGameRules().SetGameOver();
                    if( !IsDebug() )
                        PlaySound(Constants.SOUND_LOOSE);
                    if( !IsDebug() )
                        m_ui.ShowEndMenu(false);
                    else
                        Reset();
                    break;

                case EventList.EVENT_PAUSE:
                    PlaySound(Constants.SOUND_CLICK);
                    m_bGamePaused = !m_bGamePaused;
                    GetGameRules().SetPauseState(m_bGamePaused);
                    if (IsDebug() && !IsPaused())
                        m_playField.RebuildWorld();
                    else if (IsDebug() && IsPaused())
                        m_playField.ClearWorld();
                    break;

                case EventList.EVENT_COLLIDEWORLD:
                    if (m_gameRules.IsWallSolid())
                        SendEvent(EventList.EVENT_GAMEOVER);
                    else
                        m_snake.WarpSnake();
                    break;

                case EventList.EVENT_EATEN:
                    m_snake.SetAte();
                    m_playField.SpawnFood();
                    m_gameRules.AddPoints((int)(m_gameRules.GetScoreDiv()/(m_gameRules.GetSpeed()))+m_gameRules.GetEaten()+1);
                    m_gameRules.IncrementEaten();
                    PlaySound(Constants.SOUND_EAT);
                    break;

                case EventList.EVENT_COLLIDEWALL:
                    SendEvent(EventList.EVENT_GAMEOVER);
                    break;

                case EventList.EVENT_WIN:
                    m_gameRules.SetGameOver();
                    PlaySound(Constants.SOUND_WIN);
                    m_ui.ShowEndMenu(true);
                    break;
            }
        }

        /*
         * Precaches specfied image as sprite
         */
        public void PrecacheSprite(string sprite)
        {
            if (IsPrecached(sprite))
                return;

            PrecachedSprite spr = new PrecachedSprite(sprite);

            if (!spr.IsPrecached() || m_precahcedSprites == null)
                return;

            m_precahcedSprites.Add(sprite, spr);
        }

        /*
         * Returns, if possible, the sprite that was precached that was requested
         */
        public PrecachedSprite GetPrecachedSprite(string sprite)
        {
            if (!IsPrecached(sprite) || m_precahcedSprites == null)
                return null;

            PrecachedSprite spr = null;

            if (m_precahcedSprites.TryGetValue(sprite, out spr))
            {
                return spr;
            }

            return spr;
        }

        /*
         * Checks if sprite was precached
         */
        public bool IsPrecached(string sprite)
        {
            if (m_precahcedSprites == null)
                return false;

            return m_precahcedSprites.ContainsKey(sprite);
        }

        /*
         * Adds input states
         */
        public void SendInput(Keys code, bool up, bool mouse = false)
        {
            if (code == Keys.Space && up && !m_gameRules.IsGameOver() && !IsPaused())
                Reset();

            if (code == Keys.Escape && !m_gameRules.IsGameOver())
                HandleShowMenu(up);

            if (code == Keys.F1 && IsDebug())
                m_parent.KillGame();

            if (mouse)
            {
                m_bNeedMouseReset = true;
            }
            else if (!mouse && m_bNeedMouseReset)
            {
                m_input.OverrideInput(0);
                m_bNeedMouseReset = false;
            }

            if (!up)
            {
                if (code == Keys.Up)
                    m_input.AddInput(Input_T.IN_UP);

                if (code == Keys.Down)
                    m_input.AddInput(Input_T.IN_DOWN);

                if (code == Keys.Left)
                    m_input.AddInput(Input_T.IN_LEFT);

                if (code == Keys.Right)
                    m_input.AddInput(Input_T.IN_RIGHT);
            }
            else
            {
                if (code == Keys.Up)
                    m_input.RemoveInput(Input_T.IN_UP);

                if (code == Keys.Down)
                    m_input.RemoveInput(Input_T.IN_DOWN);

                if (code == Keys.Left)
                    m_input.RemoveInput(Input_T.IN_LEFT);

                if (code == Keys.Right)
                    m_input.RemoveInput(Input_T.IN_RIGHT);
            }
        }

        /*
         * Things that should happen before everything else
         */
        private void PreThink()
        {
            HandleInputs();
            HandleCollision();
        }

        /*
         * Tick
         */
        private void Think()
        {
        }

        /*
         * Things that should be done just before rendering
         */
        private void PostThink()
        {
            HandleLogic();
        }

        /*
         * Creates spotlight for snake
         */
        public void CreateSpotlight( Vector2d pos )
        {
            if (!m_gameRules.IsSpotlightEnabled())
                return;

            int d = m_gameRules.GetSpotLightSize();
            int x = pos.x - (Constants.SPRITE_SIZE / 2);
            int y = pos.y - (Constants.SPRITE_SIZE / 2);

            float spotR = (float)(d / 2);

            SolidBrush b = new SolidBrush(Color.Black);

            m_spotlight.GetFX().Clear(Constants.COLOUR_SPOTLIGHT);

            GraphicsPath screen = new GraphicsPath();
            GraphicsPath spotlight = new GraphicsPath();

            GraphicsPath[] transit = new GraphicsPath[3];

            screen.AddRectangle(Rectangle.FromLTRB(0, 0, m_spotlight.GetBuffer().Width, m_spotlight.GetBuffer().Height));
            spotlight.AddEllipse((float)(x - d / 4), (float)(y - d / 4), spotR, spotR);
            Region region = new Region(screen);
            region.Exclude(spotlight);

            float leftTrans = x - (spotR / 2);
            float rightTrans = (m_spotlight.GetBuffer().Width - x - (spotR / 2));
            float upTrans = y - (spotR / 2);
            float downTrans = (m_spotlight.GetBuffer().Height - y - (spotR / 2));

            float transX = (x - d / 4);
            float transY = (y - d / 4);
            bool shouldTransitX = true;
            bool shouldTransitY = true;
            
            // Left
            if (leftTrans < 0)
                transX = m_spotlight.GetBuffer().Width + leftTrans;
            // Right
            else if (rightTrans < 0)
                transX = -rightTrans - spotR;
            else
                shouldTransitX = false;

            // Up
            if (upTrans < 0) 
                transY = m_spotlight.GetBuffer().Height + upTrans;
            // Down
            else if (downTrans < 0) 
                transY = -downTrans - spotR;
            // None
            else 
                shouldTransitY = false;

            //Triple transition
            if (shouldTransitX && shouldTransitY)
            {
                for( int i = 0; i < 3; i++ )
                    transit[i] = new GraphicsPath();

                transit[0].AddEllipse(transX, transY, spotR, spotR);
                transit[1].AddEllipse((x - d / 4), transY, spotR, spotR);
                transit[1].AddEllipse(transX, (y - d / 4), spotR, spotR);

                for (int i = 0; i < 3; i++)
                    region.Exclude(transit[i]);
            }
            //Single transition
            else if (shouldTransitX || shouldTransitY)
            {
                transit[0] = new GraphicsPath();
                transit[0].AddEllipse(transX, transY, spotR, spotR);
                region.Exclude(transit[0]);
            }

            m_spotlight.GetFX().FillRegion(b, region);
        }

        /*
         * Renders the world
         */
        public void RenderWorld(Graphics fx)
        {
            //Clear the screen
            fx.Clear(Color.Black);

            //Draw background
            m_background.Draw();

            //Draw food
            m_playField.DrawPickups();

            //Draw snake
            m_snake.Draw();

            //Draw field
            if (m_gameRules.IsWallSolid())
                m_playField.Draw();

            //Draw UI
            m_gui.GetFX().Clear(Color.Transparent);
            m_ui.Draw();

            //Render the scene
            m_fx.Render(fx);

            //Render after-effects
            m_spotlight.Render(fx);

            //Render user interface
            m_gui.Render(fx);
        }

        /*
         * Decides movement vector according to inputs
         */
        private void HandleInputs()
        {
            int curIn = m_input.GetCurrentInput();

            Vector2d moveVec = new Vector2d(0, 0);

            if (BitWiseOperator.HasBit(curIn, (int)Input_T.IN_UP))
                moveVec.y--;

            if (BitWiseOperator.HasBit(curIn, (int)Input_T.IN_DOWN))
                moveVec.y++;

            if (BitWiseOperator.HasBit(curIn, (int)Input_T.IN_LEFT))
                moveVec.x--;

            if (BitWiseOperator.HasBit(curIn, (int)Input_T.IN_RIGHT))
                moveVec.x++;

            m_snake.MoveSnake(moveVec);
        }

        /*
         * Handles world collision
         */
        private void HandleCollision()
        {
            m_collisionManager.ApplyCollision();
        }

        /*
         * Handles world logic
         */
        private void HandleLogic()
        {
            //Update UI
            m_ui.UpdateUI();
        }

        /*
         * For map creating
         */
        public void DebugDrawMap(Vector2d pos, bool delete)
        {
            if (IsPaused())
                return;

            if (delete)
            {
                m_playField.DebugRemoveEntity(pos);
                return;
            }

            m_playField.DebugPutEntity(pos, m_iCurrentBuild);
        }

        /*
         * Handles menu open/close events
         */
        private void HandleShowMenu(bool released)
        {
            if (!m_bEscapeReleased && !released)
                return;
            else if (!m_bEscapeReleased && released)
            {
                m_bEscapeReleased = true;
                return;
            }
            else if (released)
                return;

            m_bEscapeReleased = false;
            m_bMenuVisible = !m_bMenuVisible;
            m_ui.ShowMenu(m_bMenuVisible);
            SendEvent(EventList.EVENT_PAUSE);
        }

        /*
         * Returns to title screen
         */
        public void QuitGame(bool save = false)
        {
            if (save && IsDebug())
                if (!SaveMap())
                    return;

            m_parent.KillGame();
        }

        /*
         * Saves map
         */
        private bool SaveMap()
        {
            SaveFileDialog diag = new SaveFileDialog();
            diag.Filter = Constants.MAP_FORMAT;
            diag.Title = "Save map as...";
            DialogResult result = diag.ShowDialog();

            if (result != DialogResult.OK)
                return false;

            using (XmlWriter save = XmlWriter.Create(diag.FileName))
            {
                save.WriteStartDocument();
                {
                    save.WriteStartElement("Map");
                    {
                        save.WriteComment("THIS WAS GENERATED AUTOMATICALLY, DO NOT MODIFY MANUALLY!");
                        save.WriteComment("Map format version");
                        save.WriteStartElement("Version");
                        save.WriteValue(Constants.MAP_VERSION);
                        save.WriteEndElement();

                        save.WriteComment("Begin entity block");
                        save.WriteStartElement("Entities");
                        {
                            save.WriteComment("Total entities");
                            save.WriteStartElement("Total");
                            save.WriteValue(GetPlayField().GetDrawList().Count);
                            save.WriteEndElement();

                            save.WriteComment("Begin entity list");
                            save.WriteStartElement("List");
                            {
                                foreach (KeyValuePair<Vector2d, BaseEntity> item in GetPlayField().GetDrawList())
                                {
                                    save.WriteStartElement("Entity");
                                    {
                                        save.WriteStartElement("Pos", "x");
                                        save.WriteValue(item.Value.GetPos().x);
                                        save.WriteEndElement();

                                        save.WriteStartElement("Pos", "y");
                                        save.WriteValue(item.Value.GetPos().y);
                                        save.WriteEndElement();

                                        save.WriteStartElement("Type");
                                        save.WriteValue((int)item.Value.GetTileType());
                                        save.WriteEndElement();
                                    }
                                    save.WriteEndElement();
                                }
                            }
                            save.WriteComment("End entity list");
                            save.WriteEndElement();
                        }
                        save.WriteComment("End entity block");
                        save.WriteEndElement();
                    }
                    save.WriteComment("END OF MAP FILE");
                    save.WriteEndElement();
                }
                save.WriteEndDocument();
            }
            return true;
        }

        /*
         * Loads map
         */
        private void LoadMap()
        {
            try
            {
                XmlReader load = XmlReader.Create(m_parent.GetMapName());

                if (load == null)
                    return;

                try
                {
                    load.ReadStartElement("Map");
                    {

                        load.ReadStartElement("Version");
                        int version = load.ReadContentAsInt();
                        load.ReadEndElement();

                        if (version != Constants.MAP_VERSION)
                            return;

                        load.ReadStartElement("Entities");
                        {
                            load.ReadStartElement("Total");
                            int total = load.ReadContentAsInt();
                            load.ReadEndElement();

                            if (total <= 0)
                                return;

                            load.ReadStartElement("List");
                            {
                                for (int i = 0; i < total; i++)
                                {
                                    Vector2d pos = new Vector2d();
                                    TileList tile = TileList.TILE_NONE;

                                    load.ReadStartElement("Entity");
                                    {
                                        load.ReadStartElement("Pos", "x");
                                        pos.x = load.ReadContentAsInt();
                                        load.ReadEndElement();

                                        load.ReadStartElement("Pos", "y");
                                        pos.y = load.ReadContentAsInt();
                                        load.ReadEndElement();

                                        load.ReadStartElement("Type");
                                        tile = (TileList)load.ReadContentAsInt();
                                        load.ReadEndElement();
                                    }
                                    load.ReadEndElement();

                                    SetCurrentBuild(tile);
                                    DebugDrawMap(pos, false);
                                }
                            }
                            load.ReadEndElement();
                        }
                        load.ReadEndElement();
                    }
                    load.ReadEndElement();
                }
                catch (XmlException) { }
            }
            catch (System.IO.FileNotFoundException) { }

            m_playField.GetDrawList();
        }

        /*
         * Restart game
         */
        public void Restart()
        {
            Reset();
        }

        /*
         * Play sound
         */
        public void PlaySound(string sound)
        {
            SoundPlayer plr = new SoundPlayer(sound);
            plr.LoadAsync();
            
            if (plr.IsLoadCompleted)
                plr.Play();
        }
    }
}
